#!/usr/bin/python
#
# Copyright (c) 2009 Julius Volz
# See LICENSE for details.

import elixir
import sqlalchemy as sa
import model
import time
from threading import Thread

from soaplib.wsgi_soap import SimpleWSGISoapApp
from soaplib.service import soapmethod
from soaplib.serializers.primitive import String, Integer, Array

from soaplib.client import make_service_client
from silk.sync.target import sync as tgt_sync
from ... import config
import soap


def init():
  elixir.metadata.bind = "sqlite:///lmp_source.sqlite"
  elixir.setup_all()
  elixir.create_all()

def flush_links(linkspecs):
  # cancel all subscriptions
  for ls in linkspecs.values():
    if ls.sync:
      client = make_service_client(ls.sync['dst_endpoint'],
                                   tgt_sync.LinkSyncService())
      client.cancel_subscription(ls.sync['src_endpoint'])

  # flush links in DB
  for link in model.Link().query.all():
    link.delete()

  elixir.session.commit()

def add_links(linkspec_id, links, link_type):
  for lnk in links:
    link = model.Link()
    link.source = lnk['src']
    link.target = lnk['dst']
    link.linktype = link_type
    link.linkspec = linkspec_id
    # TODO: detect which resources are relevant to link
    # only monitor link target for now:
    link.monitored.append(model.MonitoredResource(resource=lnk['dst']))

  elixir.session.commit()

def transfer_links():
  linkspec_links = {}
  for lnk in model.Link.query.all():
    if lnk.linkspec not in linkspec_links:
      linkspec_links[lnk.linkspec] = []

    link = soap.Link()
    link.source = lnk.source
    link.linktype = lnk.linktype
    link.target = lnk.target
    link.subscribe = []
    for monitored in lnk.monitored:
      link.subscribe.append(monitored.resource)
    linkspec_links[lnk.linkspec].append(link)

  for ls_id in linkspec_links:
    ls = config.linkspecs[ls_id]
    client = make_service_client(ls.sync['dst_endpoint'],
                                 tgt_sync.LinkSyncService())

    client.link_notification(ls.sync['src_endpoint'], linkspec_links[ls_id])


class LinkSyncService(SimpleWSGISoapApp):

  @soapmethod(soap.ChangeNotification)
  def change_notification(self, change_msg):
    # TODO: update last remote seqnum
    for chg in change_msg.changes:
      change = model.ResourceChange()
      change.seqnum = chg.seqnum
      change.resource = chg.resource
      change.changetype = chg.changetype
    elixir.session.commit()


class SoapService(Thread):
  def run(self):
    from cherrypy.wsgiserver import CherryPyWSGIServer
    server = CherryPyWSGIServer(('localhost', 7788), LinkSyncService())
    try:
      server.start()
    except KeyboardInterrupt:
      server.stop()


def last_seqnum():
  try:
    # TODO: how to use MAX() with elixir instead of this?
    return model.ResourceChange.query.order_by(sa.desc(model.ResourceChange.seqnum)).first().seqnum
  except:
    return 0

class ChangeMonitor(Thread):
  def run(self):
    self.last_seqnum = last_seqnum()
    while True:
      time.sleep(2)
      print "Synching changes... ",
      self.sync_changes()

  def sync_changes(self):
    # get all changes after last processed seqnum
    changes = model.ResourceChange.query.filter(
      model.ResourceChange.seqnum > self.last_seqnum
    ).all()

    # update up to which seqnum we have seen changes
    self.last_seqnum = last_seqnum() # TODO: race condition

    print 'found %s, seqnum is %s' % (len(changes), self.last_seqnum)

    # find and recalculate links affected by each change, remember those with
    # similarity < accept_threshold to be deleted afterwards
    recalculated_links = set() # used to avoid double recalculation
    delete_links = set()
    for change in changes:
      print 'New change: [%s] %s' % (change.changetype, change.resource)
      # is the changed resource monitored? (could be monitored by several links)
      monitored = model.MonitoredResource.query.filter_by(resource=change.resource)
      for mon in monitored:
        # recalculate each affected link ONCE (even if affected by several changes)
        if mon.link not in recalculated_links:
          print 'Recalculating link (%s -> %s)... ' % (mon.link.source,
                                                       mon.link.target),
          valid = config.linkspecs[mon.link.linkspec].validate_link(mon.link.source,
                                                                    mon.link.target)
          recalculated_links.add(mon.link)

          if not valid:
            print 'INVALID'
            delete_links.add(mon.link)
          else:
            print 'VALID'

    soap_delete_links = {}
    for link in delete_links:
      lnk = soap.Link()
      lnk.source = link.source
      lnk.linktype = link.linktype
      lnk.target = link.target

      if link.linkspec not in soap_delete_links:
        soap_delete_links[link.linkspec] = []
      soap_delete_links[link.linkspec].append(lnk)

      # delete link from local DB
      link.delete()

    elixir.session.commit()

    # delete links remotely
    for ls in soap_delete_links:
      linkspec = config.linkspecs[ls]
      client = make_service_client(linkspec.sync['dst_endpoint'],
                                   tgt_sync.LinkSyncService())
      client.link_deletion_notification(soap_delete_links[ls])


def run_daemon(transfer=True):
  if transfer:
    transfer_links()
  soap_service = SoapService()
  change_monitor = ChangeMonitor()
  soap_service.start()
  change_monitor.start()
  try:
    while True:
      time.sleep(1000)
  except KeyboardInterrupt:
    exit()
